Item8-别让异常逃离析构函数
Item8-别让异常逃离析构函数
析构函数中发生异常是件棘手的事
由于析构函数常常被自动调用,在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为
class Widget {
public:
...
~Widget() { ... } // assume this might emit an exception
};
void doSomething()
{
std::vector<Widget> v;
...
}
当 v 被析构时,它有责任析构它包含的所有 Widgets。假设 v 中有十个 Widgets,在第一个的析构过程中,抛出一个异常。其它 9 个 Widgets 仍然必须被析构,否则它们持有的所有资源将被泄漏。这时如果第二个 Widget 析构又抛出异常,现在有两个同时活动的异常,程序若不是结束执行就是引发未定义行为。
假设使用一个类负责数据库连接:
class DBConnection {
public:
...
static DBConnection create();
void close();
};
为了确保客户不会忘记在对象上调用 close,一个合理的主意是为 DBConnection 建立一个资源管理类,在它的析构函数中调用 close。
class DBConn {
public:
...
~DBConn()
{
db.close();
}
private:
DBConnection db;
};
使用时:
{
DBConn dbc(DBConnection::create());
...
}
如果 DBConn 析构函数调用close导致异常,则析构函数就会传播该异常,也就是允许异常离开这个析构函数,这会造成麻烦。
析构函数中处理异常的两种思路
有两个办法可以避免这一问题,DBConn的析构函数可以:
如果close抛出异常就结束程序,通常通过调用 abort完成:
DBConn::~DBConn() { try { db.close(); } catch (...) { make log entry that the call to close failed; std::abort(); } }
吞下因调用close而发生的异常:
DBConn::~DBConn() { try { db.close(); } catch (...) { make log entry that the call to close failed; } }
如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理的选项,毕竟它可以阻止异常从析构函数传播出去从而导致未定义行为。
一般而言,吞掉异常是个坏主意,因为它压制了“某些动作失败”的重要信息,然而有些时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。
提供类用户异常处理接口
在遭遇并忽略了一个错误后,程序必须能够继续可靠地执行,这才是一个可行的方案。
一个极佳的策略是重新设计 DBConn的接口,使其客户有机会对可能出现的问题作出反应。
class DBConn {
public:
...
void close() //@ new function for client use
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed) {
try { //@ close the connection if the client didn't
db.close();
}
catch (...) { //@ if closing fails,note that and terminate or swallow
make log entry that call to close failed;
...
}
}
private:
DBConnection db;
bool closed;
};
将调用 close 的责任从析构函数移交给 DBConn 的客户,同时在 DBConn 的析构函数中包含一个“候补”调用。
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常就是危险,总会带来“过早结束程序”或“发生不明确行为”的风险。所以,让客户自己调用 close 并不是对他们带来负担,而是给他们一个处理错误的机会,否则他们没有机会响应。
总结
- 析构函数绝对不要吐出异常。如果析构函数调用了可能抛出异常的函数,析构函数应该捕捉所有异常,然后吞下它们(不传播)或者结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通的函数(而非析构函数)执行该操作。